12. 训练模型

超参数

定义模型之后,下一步是用超参数实例化模型。

# Instantiate the model w/ hyperparams
vocab_size = len(vocab_to_int)+1 # +1 for the 0 padding + our word tokens
output_size = 1
embedding_dim = 400
hidden_dim = 256
n_layers = 2

net = SentimentRNN(vocab_size, output_size, embedding_dim, hidden_dim, n_layers)

print(net)

步骤和之前差不多,但是要注意 vocab_size

它等于 vocab_to_int 字典的长度(所有唯一字词数量)加一,因为我们在填充输入特征时添加了标记 0。预处理数据之后,这个超参数需要再加上一两个额外的特殊标记。

output_size 设为 1,输出将是一个 0-1 之间的 S 型函数值,表示影评是正面还是负面影评。

然后是嵌入和隐藏维度。嵌入维度只能代表 70000 个字词的一小部分,200-500 之间的任何一个值都行。我选择的是 400。同理,对于隐藏维度,256 个隐藏特征应该足够区分正面和负面影评了。

我将创建两个 LSTM 层级。最后实例化模型并输出模型,看看一切是否正常。

模型超参数

模型超参数

训练和优化

训练代码应该看起来很熟悉了。不过有一个不同之处:我们将使用适合单个 S 型函数输出的新交叉熵损失。

BCELoss二元交叉熵损失)会对 0-1 之间的单个值应用交叉熵损失。

同样使用 Adam 优化器。

# loss and optimization functions
lr=0.001

criterion = nn.BCELoss()
optimizer = torch.optim.Adam(net.parameters(), lr=lr)

输出,目标格式

另外请注意,在训练循环里,我们通过运行 output.squeeze() 使输出没有空维度,并且通过 labels.float() 将标签设为浮点数张量。然后执行反向传播步骤。

训练和评估模式

从下面可以看出,当我们用数据训练模型时和用数据评估模型时,会在训练模式和评估模式之间切换。

训练循环

下面是一个常见的训练循环。

我将只训练 4 个周期,因为我发现 4 个周期后验证损失停止下降了。

  • 先初始化隐藏状态,然后进入批次循环,接着将隐藏状态与其历史记录分离,并执行反向传播步骤。
  • 从 train_dataloader 中获取输入和标签数据。然后将模型应用到输入上并比较输出和真实标签。
  • 我还添加了一些检查模型在验证集上效果的代码,通过这些代码可以判断何时停止训练或哪个模型效果最好,值得保存下来。
# training params

epochs = 4 # 3-4 is approx where I noticed the validation loss stop decreasing

counter = 0
print_every = 100
clip=5 # gradient clipping

# move model to GPU, if available
if(train_on_gpu):
    net.cuda()

net.train()
# train for some number of epochs
for e in range(epochs):
    # initialize hidden state
    h = net.init_hidden(batch_size)

    # batch loop
    for inputs, labels in train_loader:
        counter += 1

        if(train_on_gpu):
            inputs, labels = inputs.cuda(), labels.cuda()

        # Creating new variables for the hidden state, otherwise
        # we'd backprop through the entire training history
        h = tuple([each.data for each in h])

        # zero accumulated gradients
        net.zero_grad()

        # get the output from the model
        output, h = net(inputs, h)

        # calculate the loss and perform backprop
        loss = criterion(output.squeeze(), labels.float())
        loss.backward()
        # `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
        nn.utils.clip_grad_norm_(net.parameters(), clip)
        optimizer.step()

        # loss stats
        if counter % print_every == 0:
            # Get validation loss
            val_h = net.init_hidden(batch_size)
            val_losses = []
            net.eval()
            for inputs, labels in valid_loader:

                # Creating new variables for the hidden state, otherwise
                # we'd backprop through the entire training history
                val_h = tuple([each.data for each in val_h])

                if(train_on_gpu):
                    inputs, labels = inputs.cuda(), labels.cuda()

                output, val_h = net(inputs, val_h)
                val_loss = criterion(output.squeeze(), labels.float())

                val_losses.append(val_loss.item())

            net.train()
            print("Epoch: {}/{}...".format(e+1, epochs),
                  "Step: {}...".format(counter),
                  "Loss: {:.6f}...".format(loss.item()),
                  "Val Loss: {:.6f}".format(np.mean(val_losses)))

一定要看看训练损失和验证损失在训练过程中是如何降低的。对训练过的模型满意后,可以通过多种方式测试模型,看看模型在新数据上表现如何。

查看 Solution 代码

要仔细查看 solution 代码,请转到 solution workspace 或点击此处查看网页版。